package cn.bitkit.tools.net;

import cn.bitkit.base.util.LocalDateTimeUtil;
import cn.bitkit.tools.common.IOUtil;
import cn.bitkit.tools.file.FileUtil;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;

import java.io.File;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * okhttp
 * @author changbo
 */
@Slf4j
public class HttpHelper {

	public static final int DEFAULT_TIMEOUT = 10;
	public static final int LONG_TIMEOUT = 60;
	public static final int GET = 0;
	public static final int POST = 1;
	
	public static HttpHelper.Http.HttpBuilder get(){
		return Http.builder().method(GET);
	}
	
	public static HttpHelper.Http.HttpBuilder post(){
		return Http.builder().method(POST);
	}

	@Data
	@Builder
	public static class Http {
		private int method;
		private String url;
		@Singular
		private Map<String, String> headers;
		@Singular
		private Map<String, String> params;
		@Singular
		private List<HttpMultipart> multiparts;
		private String body;
		private int timeout;
		private byte[] certFile;
		private String certKey;
		private String authUsername;
		private String authPassword;
		private String proxyIp;
		private int proxyPort;
		private String proxyUsername;
		private String proxyPassword;
		//
		private HttpResponse response;
		private OutputStream outputStream;
		private String outputFile;
		private String outputFolder;
		private boolean outputByte;

		@SneakyThrows
		public HttpResponse execute(){
			initParameter();
			Request.Builder reqBuilder = new Request.Builder();
			if(method == GET){
				initGet(reqBuilder);
			}
			if(method == POST){
				initPost(reqBuilder);
			}
			initHeader(reqBuilder);
			OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
			initClient(clientBuilder);
			OkHttpClient client = clientBuilder.build();
			HttpResponse response = new HttpResponse();
			try {
				Response resp = client.newCall(reqBuilder.build()).execute();
				initResponse(resp, response);
			} catch (Exception e) {
				log.error("", e);
			}
			return response;
		}

		private void initParameter(){
			if(params == null){
				setParams(new HashMap<>(4));
			}else {
				params = params.entrySet().stream()
						.filter(entry -> entry.getValue() != null)
						.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
			}
			if(headers == null){
				setHeaders(new HashMap<>(4));
			}else {
				headers = headers.entrySet().stream()
						.filter(entry -> entry.getValue() != null)
						.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
			}
			if(multiparts == null){
				setMultiparts(new ArrayList<>(4));
			}else{
				multiparts = multiparts.stream().filter(e -> e.file != null).toList();
			}
		}

		private void initGet(Request.Builder reqBuilder){
			HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
			if(params != null && !params.isEmpty()){
				params.forEach(urlBuilder::addQueryParameter);
			}
			reqBuilder.url(urlBuilder.build());
			reqBuilder.get();
		}

		private void initPost(Request.Builder reqBuilder){
			reqBuilder.url(url);
			RequestBody reqBody = null;
			if(body != null) {
				reqBody = RequestBody.create(body, MediaType.parse("application/json"));
			}
			if(!multiparts.isEmpty()) {
				MultipartBody.Builder formBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
				params.forEach(formBuilder::addFormDataPart);
				multiparts.forEach(part -> {
					RequestBody fileBody = RequestBody.create(part.getFile(), MediaType.parse("application/octet-stream"));
					formBuilder.addFormDataPart(part.getKey(), part.getFilename(), fileBody);
				});
				reqBody = formBuilder.build();
			}
			if(multiparts.isEmpty() && !params.isEmpty()) {
				FormBody.Builder formBuilder = new FormBody.Builder();
				params.forEach(formBuilder::add);
				reqBody = formBuilder.build();
			}
			reqBuilder.post(reqBody);
		}

		private void initHeader(Request.Builder reqBuilder){
			headers.forEach(reqBuilder::addHeader);
			if(proxyUsername != null) {
				reqBuilder.header("Proxy-Authorization", Credentials.basic(proxyUsername, proxyPassword));
			}
			if(authUsername != null) {
				reqBuilder.header("Authorization", Credentials.basic(authUsername, authPassword));
			}
		}

		private void initClient(OkHttpClient.Builder clientBuilder){
			if(timeout == 0){
				timeout = DEFAULT_TIMEOUT;
				if(multiparts != null || outputStream != null || outputFile != null) {
					timeout = LONG_TIMEOUT;
				}
			}
			clientBuilder.connectTimeout(timeout, TimeUnit.SECONDS);
			clientBuilder.readTimeout(timeout, TimeUnit.SECONDS);
			clientBuilder.writeTimeout(timeout, TimeUnit.SECONDS);
			if(certFile != null) {
				clientBuilder.sslSocketFactory(SslUtil.createSSLSocketFactory(certFile, certKey), SslUtil.createTrustManager());
			}else {
				clientBuilder.sslSocketFactory(SslUtil.createSSLSocketFactory(), SslUtil.createTrustManager());
			}
			clientBuilder.hostnameVerifier(SslUtil.createHostnameVerifier());
			if(proxyIp != null) {
				clientBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyIp, proxyPort)));
			}
			clientBuilder.retryOnConnectionFailure(false);
			clientBuilder.followRedirects(true);
		}

		private void initResponse(Response resp, HttpResponse response) throws Exception{
			response.setCode(resp.code());
			if(!resp.isSuccessful() || resp.body() == null) {
				log.error("response.code:{}", response.getCode());
				return;
			}
			if(outputStream != null) {
				IOUtil.write(resp.body().byteStream(), outputStream);
				return;
			}
			if(outputFile != null) {
				FileUtil.writeFile(new File(outputFile), resp.body().byteStream());
				return;
			}
			if(outputFolder != null) {
				String contentDisposition = resp.header("Content-Disposition");
				String filename = ParamUtil.textAsMap(contentDisposition).get("filename");
				filename = filename == null ? LocalDateTimeUtil.getCurrentTimeNum() : URLDecoder.decode(filename, StandardCharsets.UTF_8);
				FileUtil.writeFile(new File(outputFolder+"/"+filename), resp.body().byteStream());
				return;
			}
			if(outputByte) {
				response.setByteBody(resp.body().bytes());
				return;
			}
			response.setBody(resp.body().string());
			response.setHeaderMap(resp.headers().toMultimap());
		}
	}

	@Data
	public static class HttpResponse {
		private int code;
		private String msg;
		private String body;
		private byte[] byteBody;
		private Map<String, List<String>> headerMap;
	}

	@Data
	@Builder
	@AllArgsConstructor
	public static class HttpMultipart {
		private String key;
		private String filename;
		private byte[] file;
	}
	
}
